--- /dev/null
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const Workspace = imports.ui.workspace;
+
+// Fraction of original screen size for thumbnails
+let THUMBNAIL_SCALE = 1/8.;
+
+function WindowClone(realWindow) {
+ this._init(realWindow);
+}
+
+WindowClone.prototype = {
+ _init : function(realWindow) {
+ this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
+ reactive: true,
+ x: realWindow.x,
+ y: realWindow.y });
+ this.actor._delegate = this;
+ this.realWindow = realWindow;
+ this.metaWindow = realWindow.meta_window;
+
+ this.actor.connect('button-release-event',
+ Lang.bind(this, this._onButtonRelease));
+
+ this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+ this._draggable = DND.makeDraggable(this.actor,
+ { restoreOnSuccess: true,
+ dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
+ dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY });
+ this._draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
+ this._draggable.connect('drag-end', Lang.bind(this, this._onDragEnd));
+ this.inDrag = false;
+
+ this._selected = false;
+ },
+
+ setStackAbove: function (actor) {
+ this._stackAbove = actor;
+ if (this._stackAbove == null)
+ this.actor.lower_bottom();
+ else
+ this.actor.raise(this._stackAbove);
+ },
+
+ destroy: function () {
+ this.actor.destroy();
+ },
+
+ _onDestroy: function() {
+ this.actor._delegate = null;
+
+ if (this.inDrag) {
+ this.emit('drag-end');
+ this.inDrag = false;
+ }
+
+ this.disconnectAll();
+ },
+
+ _onButtonRelease : function (actor, event) {
+ this._selected = true;
+ this.emit('selected', event.get_time());
+ },
+
+ _onDragBegin : function (draggable, time) {
+ this.inDrag = true;
+ this.emit('drag-begin');
+ },
+
+ _onDragEnd : function (draggable, time, snapback) {
+ this.inDrag = false;
+
+ // We may not have a parent if DnD completed successfully, in
+ // which case our clone will shortly be destroyed and replaced
+ // with a new one on the target workspace.
+ if (this.actor.get_parent() != null) {
+ if (this._stackAbove == null)
+ this.actor.lower_bottom();
+ else
+ this.actor.raise(this._stackAbove);
+ }
+
+
+ this.emit('drag-end');
+ }
+};
+Signals.addSignalMethods(WindowClone.prototype);
+
+
+/**
+ * @metaWorkspace: a #Meta.Workspace
+ */
+function WorkspaceThumbnail(metaWorkspace) {
+ this._init(metaWorkspace);
+}
+
+WorkspaceThumbnail.prototype = {
+ _init : function(metaWorkspace) {
+ this.metaWorkspace = metaWorkspace;
+
+ this.actor = new St.Bin({ reactive: true,
+ clip_to_allocation: true,
+ style_class: 'workspace-thumbnail' });
+ this.actor._delegate = this;
+
+ this._group = new Clutter.Group();
+ this.actor.add_actor(this._group);
+
+ this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+ this.actor.connect('button-press-event', Lang.bind(this,
+ function(actor, event) {
+ return true;
+ }));
+ this.actor.connect('button-release-event', Lang.bind(this,
+ function(actor, event) {
+ this.metaWorkspace.activate(event.get_time());
+ return true;
+ }));
+
+ this._background = new Clutter.Clone({ source: global.background_actor });
+ this._group.add_actor(this._background);
+
+ this._group.set_size(THUMBNAIL_SCALE * global.screen_width, THUMBNAIL_SCALE * global.screen_height);
+ this._group.set_scale(THUMBNAIL_SCALE, THUMBNAIL_SCALE);
+
+ let windows = global.get_window_actors().filter(this._isMyWindow, this);
+
+ // Create clones for windows that should be visible in the Overview
+ this._windows = [];
+ for (let i = 0; i < windows.length; i++) {
+ if (this._isOverviewWindow(windows[i])) {
+ this._addWindowClone(windows[i]);
+ }
+ }
+
+ // Track window changes
+ this._windowAddedId = this.metaWorkspace.connect('window-added',
+ Lang.bind(this, this._windowAdded));
+ this._windowRemovedId = this.metaWorkspace.connect('window-removed',
+ Lang.bind(this, this._windowRemoved));
+ },
+
+ _lookupIndex: function (metaWindow) {
+ for (let i = 0; i < this._windows.length; i++) {
+ if (this._windows[i].metaWindow == metaWindow) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ syncStacking: function(stackIndices) {
+ this._windows.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
+
+ for (let i = 0; i < this._windows.length; i++) {
+ let clone = this._windows[i];
+ let metaWindow = clone.metaWindow;
+ if (i == 0) {
+ clone.setStackAbove(this._background);
+ } else {
+ let previousClone = this._windows[i - 1];
+ clone.setStackAbove(previousClone.actor);
+ }
+ }
+ },
+
+ _windowRemoved : function(metaWorkspace, metaWin) {
+ let win = metaWin.get_compositor_private();
+
+ // find the position of the window in our list
+ let index = this._lookupIndex (metaWin);
+
+ if (index == -1)
+ return;
+
+ let clone = this._windows[index];
+ this._windows.splice(index, 1);
+ clone.destroy();
+ },
+
+ _windowAdded : function(metaWorkspace, metaWin) {
+ if (this.leavingOverview)
+ return;
+
+ let win = metaWin.get_compositor_private();
+
+ if (!win) {
+ // Newly-created windows are added to a workspace before
+ // the compositor finds out about them...
+ Mainloop.idle_add(Lang.bind(this,
+ function () {
+ if (this.actor && metaWin.get_compositor_private())
+ this._windowAdded(metaWorkspace, metaWin);
+ return false;
+ }));
+ return;
+ }
+
+ if (!this._isOverviewWindow(win))
+ return;
+
+ let clone = this._addWindowClone(win);
+ },
+
+ destroy : function() {
+ this.actor.destroy();
+ },
+
+ _onDestroy: function(actor) {
+ this.metaWorkspace.disconnect(this._windowAddedId);
+ this.metaWorkspace.disconnect(this._windowRemovedId);
+
+ this._windows = [];
+ this.actor = null;
+ },
+
+ // Tests if @win belongs to this workspaces
+ _isMyWindow : function (win) {
+ return win.get_workspace() == this.metaWorkspace.index() ||
+ (win.get_meta_window() && win.get_meta_window().is_on_all_workspaces());
+ },
+
+ // Tests if @win should be shown in the Overview
+ _isOverviewWindow : function (win) {
+ let tracker = Shell.WindowTracker.get_default();
+ return tracker.is_window_interesting(win.get_meta_window());
+ },
+
+ // Create a clone of a (non-desktop) window and add it to the window list
+ _addWindowClone : function(win) {
+ let clone = new WindowClone(win);
+
+ clone.connect('selected',
+ Lang.bind(this, this._onCloneSelected));
+ clone.connect('drag-begin',
+ Lang.bind(this, function(clone) {
+ Main.overview.beginWindowDrag();
+ }));
+ clone.connect('drag-end',
+ Lang.bind(this, function(clone) {
+ Main.overview.endWindowDrag();
+ }));
+ this._group.add_actor(clone.actor);
+
+ this._windows.push(clone);
+
+ return clone;
+ },
+
+ _onCloneSelected : function (clone, time) {
+ this.metaWorkspace.activate(time);
+ },
+
+ // Draggable target interface
+ handleDragOver : function(source, actor, x, y, time) {
+ if (source.realWindow)
+ return DND.DragMotionResult.MOVE_DROP;
+ if (source.shellWorkspaceLaunch)
+ return DND.DragMotionResult.COPY_DROP;
+
+ return DND.DragMotionResult.CONTINUE;
+ },
+
+ acceptDrop : function(source, actor, x, y, time) {
+ if (source.realWindow) {
+ let win = source.realWindow;
+ if (this._isMyWindow(win))
+ return false;
+
+ let metaWindow = win.get_meta_window();
+ metaWindow.change_workspace_by_index(this.metaWorkspace.index(),
+ false, // don't create workspace
+ time);
+ return true;
+ } else if (source.shellWorkspaceLaunch) {
+ this.metaWorkspace.activate(time);
+ source.shellWorkspaceLaunch();
+ return true;
+ }
+
+ return false;
+ }
+};
+
+Signals.addSignalMethods(WorkspaceThumbnail.prototype);
const Overview = imports.ui.overview;
const Tweener = imports.ui.tweener;
const Workspace = imports.ui.workspace;
+const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
const WORKSPACE_SWITCH_TIME = 0.25;
// Note that mutter has a compile-time limit of 36
}));
controls.add(this._removeButton);
+ this._thumbnailsBox = new St.BoxLayout({ vertical: true,
+ style_class: 'workspace-thumbnails' });
+ controls.add(this._thumbnailsBox, { expand: false });
+
+ let indicator = new St.Bin({ style_class: 'workspace-thumbnail-indicator',
+ fixed_position_set: true });
+
+ // We don't want the indicator to affect drag-and-drop
+ Shell.util_set_hidden_from_pick(indicator, true);
+
+ this._thumbnailIndicator = indicator;
+ this._thumbnailsBox.add(this._thumbnailIndicator);
+ this._thumbnailIndicatorConstraints = [];
+ this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.POSITION }));
+ this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.SIZE }));
+ this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+ indicator.add_constraint(constraint);
+ });
+
this._addButton = new St.Button({ label: '+',
style_class: 'add-workspace' });
this._addButton.connect('clicked', Lang.bind(this, function() {
this._controls.show();
this._workspaces = [];
+ this._workspaceThumbnails = [];
for (let i = 0; i < global.screen.n_workspaces; i++) {
let metaWorkspace = global.screen.get_workspace_by_index(i);
this._workspaces[i] = new Workspace.Workspace(metaWorkspace);
+
+ let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+ this._workspaceThumbnails[i] = thumbnail;
+ this._thumbnailsBox.add(thumbnail.actor);
}
+ // The thumbnails indicator actually needs to be on top of the thumbnails, but
+ // there is also something more subtle going on as well - actors in a StBoxLayout
+ // are allocated from bottom to top (start to end), and we need the
+ // thumnail indicator to be allocated after the actors it is constrained to.
+ this._thumbnailIndicator.raise_top();
+
let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
let totalAllocation = this.actor.allocation;
this._nWorkspacesNotifyId =
global.screen.connect('notify::n-workspaces',
Lang.bind(this, this._workspacesChanged));
+ this._switchWorkspaceNotifyId =
+ global.window_manager.connect('switch-workspace',
+ Lang.bind(this, this._activeWorkspaceChanged));
this._restackedNotifyId =
global.screen.connect('restacked',
Lang.bind(this, this._dragEnd));
this._onRestacked();
+ this._constrainThumbnailIndicator();
+ this._zoomOut = false;
+ this._updateZoom();
},
hide: function() {
global.screen.disconnect(this._nWorkspacesNotifyId);
this._nWorkspacesNotifyId = 0;
}
+ if (this._switchWorkspaceNotifyId > 0) {
+ global.window_manager.disconnect(this._switchWorkspaceNotifyId);
+ this._switchWorkspaceNotifyId = 0;
+ }
if (this._restackedNotifyId > 0){
global.screen.disconnect(this._restackedNotifyId);
this._restackedNotifyId = 0;
this.workspacesView.destroy();
this.workspacesView = null;
+ this._unconstrainThumbnailIndicator();
for (let w = 0; w < this._workspaces.length; w++) {
this._workspaces[w].disconnectAll();
this._workspaces[w].destroy();
+ this._workspaceThumbnails[w].destroy();
}
},
+ _constrainThumbnailIndicator: function() {
+ let active = global.screen.get_active_workspace_index();
+ let thumbnail = this._workspaceThumbnails[active];
+
+ this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+ constraint.set_source(thumbnail.actor);
+ constraint.set_enabled(true);
+ });
+ },
+
+ _unconstrainThumbnailIndicator: function() {
+ this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+ constraint.set_enabled(false);
+ });
+ },
+
+ _activeWorkspaceChanged: function(wm, from, to, direction) {
+ let active = global.screen.get_active_workspace_index();
+ let thumbnail = this._workspaceThumbnails[active];
+
+ this._unconstrainThumbnailIndicator();
+ let oldAllocation = this._thumbnailIndicator.allocation;
+ this._thumbnailIndicator.x = oldAllocation.x1;
+ this._thumbnailIndicator.y = oldAllocation.y1;
+ this._thumbnailIndicator.width = oldAllocation.x2 - oldAllocation.x1;
+ this._thumbnailIndicator.height = oldAllocation.y2 - oldAllocation.y1;
+
+ Tweener.addTween(this._thumbnailIndicator,
+ { x: thumbnail.actor.allocation.x1,
+ y: thumbnail.actor.allocation.y1,
+ time: WORKSPACE_SWITCH_TIME,
+ transition: 'easeOutQuad',
+ onComplete: Lang.bind(this,
+ this._constrainThumbnailIndicator)
+ });
+ },
+
_onRestacked: function() {
let stack = global.get_window_actors();
let stackIndices = {};
}
this.workspacesView.syncStacking(stackIndices);
+ for (let i = 0; i < this._workspaceThumbnails.length; i++)
+ this._workspaceThumbnails[i].syncStacking(stackIndices);
},
_workspacesChanged: function() {
for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
let metaWorkspace = global.screen.get_workspace_by_index(w);
this._workspaces[w] = new Workspace.Workspace(metaWorkspace);
+
+ let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+ this._workspaceThumbnails[w] = thumbnail;
+ this._thumbnailsBox.add(thumbnail.actor);
}
+ this._thumbnailIndicator.raise_top();
} else {
// Assume workspaces are only removed sequentially
// (e.g. 2,3,4 - not 2,4,7)
// making its exit.
for (let l = 0; l < lostWorkspaces.length; l++)
lostWorkspaces[l].setReactive(false);
+
+ for (let k = removedIndex; k < removedIndex + removedNum; k++)
+ this._workspaceThumbnails[k].destroy();
+ this._workspaceThumbnails.splice(removedIndex, removedNum);
}
this.workspacesView.updateWorkspaces(oldNumWorkspaces,